package com.meshnetics.gb.stack.mac.io;

import gnu.io.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.microedition.io.Connection;
import javax.microedition.io.StreamConnection;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.StringTokenizer;
import java.util.TooManyListenersException;


/**
 * Connection service. Conforms to osji connection service spec
 *
 * @author Liliya Butorina
 */
public class UARTConnectionFactory implements ConnectionFactory {
    private static final Log log = LogFactory.getLog(UARTConnectionFactory.class);

    private final String[] baudNames = {"baudrate", "baud"};        // allowed synonyms of parameter
    private final String[] baudVals = {"9600", "38400", "115200"};  // allowed values of parameter
    private static final int DEFAULT_BAUD_RATE = 115200;

    private final String[] dataNames = {"databits", "data"};
    private final String[] dataVals = {"5", "6", "7", "8"};
    private static final int DEFAULT_DATA_BITS = 8;

    private final String[] stopNames = {"stopbits", "stop"};
    private final String[] stopVals = {"1", "2", "3"};
    private static final int DEFAULT_STOP_BITS = 1;

    private final String[] parityNames = {"parity"};
    private final String[] parityVals = {"none", "odd", "even", "mark", "space"};
    private static final int DEFAULT_PARITY = 0;

    private final String[] flowNames = {"flow", "flowcontrol"};
    private final String[] flowVals = {"none", "hard", "soft", "both"};
    private static final int DEFAULT_FLOW = 0;

    private final String[] timeNames = {"timeout", "time"};
    private int timeout = -1;

    private int baudRate = DEFAULT_BAUD_RATE;
    private int dataBits = DEFAULT_DATA_BITS;
    private int stopBits = DEFAULT_STOP_BITS;
    private int parity = DEFAULT_PARITY;
    private int flow = DEFAULT_FLOW;

    public String[] getSupportedProtocols() {
        return new String[]{"serial", "usb", "spi"};
    }

    public Connection createConnection(String string, int mode, boolean timeControl)
            throws IOException, IllegalArgumentException {
        // todo use mode & timeControl params ?
        if (log.isDebugEnabled()) {
            log.debug("creating connection: " + string + ", mode = " + mode + ", timeControl = " + timeControl);
        }
        CommPortIdentifier portId;
        SerialPort serialPort = null;
        URI uri;
        try {
            uri = new URI(string);
            String fileName = uri.getHost();
            if (fileName == null) {
                fileName = uri.getPath();
            }
            portId = CommPortIdentifier.getPortIdentifier(fileName);
            if (portId.getPortType() != CommPortIdentifier.PORT_SERIAL) {
                throw new IllegalArgumentException("invalid port type " + string);
            }
            serialPort = (SerialPort) portId.open("SLR Gateway", 0);
            if (log.isDebugEnabled()) {
                log.debug("port opened, owner: " + portId.getCurrentOwner());
            }
            initPort(serialPort, uri.getQuery()); //parameters baudRate, stopbits, ..
            return new SerialStreamConnection(serialPort);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("connection URI syntax invalid " + string, e);
        } catch (NoSuchPortException e) {
            throw new IllegalArgumentException("no such port " + string, e);
        } catch (PortInUseException e) {
            throw (IOException) new IOException("port in use " + string).initCause(e);
        } catch (UnsupportedCommOperationException e) {
            serialPort.close();
            throw (IOException) new IOException("failed to open " + string).initCause(e);
        } catch (TooManyListenersException e) {
            throw (IOException) new IOException("failed to open " + string).initCause(e);
        }
    }

    private void initPort(final SerialPort serialPort, String paramString) throws UnsupportedCommOperationException {
        initPortParams(paramString);
        serialPort.setSerialPortParams(baudRate, dataBits, stopBits, parity);
        if (flow == 0) {
            serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
        } else if (flow == 1) {
            serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
        } else if (flow == 2) {
            serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT);
        } else if (flow == 3) {
            serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT |
                    SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT);
        }
        // todo use timeout ?
    }

    private void initPortParams(String paramString) {
        // paramString contains parameters in format: paramName=paramVal&paramName=paramVal
        if (paramString != null) {
            String[] params = split(paramString, "&");
            for (int i = 0; i < params.length; i++) {
                String[] param = split(params[i], "=");
                if (param.length != 2) {
                    throw new IllegalArgumentException("invalid parameter " + params[i]);
                }
                String name = getName(param[0]);
                if (baudNames[0].equals(name)) {
                    baudRate = getInteger(param[1], baudVals);
                } else if (dataNames[0].equals(name)) {
                    dataBits = getInteger(param[1], dataVals);
                } else if (stopNames[0].equals(name)) {
                    stopBits = getInteger(param[1], stopVals);
                } else if (parityNames[0].equals(name)) {
                    parity = getIndexOfName(param[1], parityVals);
                } else if (flowNames[0].equals(name)) {
                    flow = getIndexOfName(param[1], flowVals);
                } else if (timeNames[0].equals(name)) {
                    timeout = Integer.getInteger(param[1], param[1].length()).intValue();
                }
            }
        }
    }

    private String[] split(String paramString, String symbol) {
        StringTokenizer st = new StringTokenizer(paramString, symbol);
        String[] res = new String[st.countTokens()];
        int i = 0;
        while (st.hasMoreTokens()) {
            res[i++] = st.nextToken();
        }
        return res;
    }

    private String getName(String name) {
        // returns non-alias name of parameter (first in corresponding array of aliases)
        String res = null;
        if (getIndexOfName(name, baudNames) >= 0) {
            res = baudNames[0];
        } else if (getIndexOfName(name, dataNames) >= 0) {
            res = dataNames[0];
        } else if (getIndexOfName(name, stopNames) >= 0) {
            res = stopNames[0];
        } else if (getIndexOfName(name, parityNames) >= 0) {
            res = parityNames[0];
        } else if (getIndexOfName(name, flowNames) >= 0) {
            res = flowNames[0];
        } else if (getIndexOfName(name, timeNames) >= 0) {
            res = timeNames[0];
        }
        return res;
    }

    private int getIndexOfName(String val, String[] dictonary) {
        for (int i = 0; i < dictonary.length; i++) {
            if (val.equals(dictonary[i])) {
                return i;
            }
        }
        return -1;
    }

    private int getInteger(String val, String[] vals) {
        int n;
        n = getIndexOfName(val, vals);
        if (n > -1) {
            return Integer.parseInt(vals[n]);
        }
        return -1;
    }

    // Inner class - SerialStreamConnection
    private final class SerialStreamConnection implements StreamConnection {
        SerialPort port;
        InputStream is = null;
        OutputStream os = null;
        private final Object dataAvailableSyncObject = new Object();

        private SerialStreamConnection(SerialPort port) throws TooManyListenersException {
            this.port = port;
            port.notifyOnDataAvailable(true);
            port.notifyOnOutputEmpty(true);
            port.addEventListener(new SerialPortEventListener() {
                public void serialEvent(SerialPortEvent e) {
                    if (e.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
                        synchronized (dataAvailableSyncObject) {
                            dataAvailableSyncObject.notifyAll();
                        }
                    }
                }
            });
        }

        public InputStream openInputStream() throws IOException {
            if (is == null) {
                is = new SerialInputStream(port.getInputStream());
                return is;
            } else {
                throw new IOException("duplicate open of " + port.getName() + " InputStream");
            }
        }

        public OutputStream openOutputStream() throws IOException {
            if (os == null) {
                os = port.getOutputStream();
                return os;
            } else {
                throw new IOException("duplicate open of " + port.getName() + " OutputStream");
            }
        }

        public DataInputStream openDataInputStream() throws IOException {
            return new DataInputStream(openInputStream());
        }

        public DataOutputStream openDataOutputStream() throws IOException {
            return new DataOutputStream(openOutputStream());
        }

        public void close() throws IOException {
            if (log.isInfoEnabled()) {
                log.info("closing UART connection");
            }
            if (os != null) {
                os.close();
                os = null;
            }
            if (is != null) {
                is.close();
                is = null;
            }
            port.close();
            if (log.isInfoEnabled()) {
                log.info("UART connection was closed");
            }
        }

        // Inner class - InputStream
        private final class SerialInputStream extends InputStream {
            protected InputStream inputStream;

            public SerialInputStream(InputStream inputStream) {
                this.inputStream = inputStream;
            }

            public int read() throws IOException {
                ensureOpen();
                waitData();
                return inputStream.read();
            }

            public int read(byte[] b, int off, int len) throws IOException {
                int res = 0;
                int n = 0;
                while (res < len && n != -1) { // not EOF
                    ensureOpen();
                    waitData();
                    n = inputStream.read(b, off + res, len - res);
                    res += n == -1 ? 0 : n;
                }
                return res;
            }

            protected void waitData() throws IOException {
                synchronized (dataAvailableSyncObject) {
                    if (inputStream != null && available() == 0) {
                        try {
                            dataAvailableSyncObject.wait();
                        } catch (InterruptedException e) {
                            throw (IOException) new IOException("read interrupted").initCause(e);
                        }
                    }
                }
            }

            public int available() throws IOException {
                return inputStream.available();
            }

            public long skip(long n) throws IOException {
                return inputStream.skip(n);
            }

            public synchronized void mark(int readlimit) {
                inputStream.mark(readlimit);
            }

            public synchronized void reset() throws IOException {
                inputStream.reset();
            }

            public boolean markSupported() {
                return inputStream.markSupported();
            }

            public void close() throws IOException {
                ensureOpen();
                synchronized (dataAvailableSyncObject) {
                    inputStream.close();
                    inputStream = null;
                    dataAvailableSyncObject.notifyAll();
                }
            }

            protected void ensureOpen() throws IOException {
                if (inputStream == null) {
                    throw new IOException("stream is closed");
                }
            }
        }
    }
}
